/*
 *     File: TestSet.cpp
 *
 *  Purpose: a test set framework for unit testing
 *  Version: 1.0      TWI 20070311
 *           - Initial version
 *  Comments on v1.1: Added expected signal handling
 *  Version: 1.1.1    AKK 20070322
 *             - Added psignal in TestSet_handle_signal() to name the (unexpected) signal
 *             + Alternatives are: strsignal or sys_siglist[]
 *             + Voor Dev-Cpp is een extra 'extern' nodig omdat psignal
 *             + daar eigenlijk niet bestaat
 *  Version: 1.1.2    TWI 20070323
 *           - Changed all tabs to spaces
 *           - Made versioning comments consistent
 *  Version: 1.2      TWI 20080603
 *           - added support for testcase and testset setup (setup once)
 *  TODO: make it C compatible again while retaining C++ compatibility
 */


/*
 *  System Includes
 */


#include <cstdio>
#include <csetjmp>
#include <csignal>
#include <cstdlib>
#include <exception>

#include "devcpp.h"


/*
 *  Module includes
 */
#include "trace.hpp"
#include "TestSet.hpp"
#include "TestSet_output.hpp"

/*
 *  Module defines
 */

#ifndef __PRETTY_FUNC__
    #define __PRETTY_FUNC__ "TestSet"
#endif

#define TESTSET_ENSURE(condition)                                            \
            if (!(condition))                                                \
            {                                                                \
                TestSet_output_ensure(__FILE__, __LINE__,                    \
                                      __PRETTY_FUNC__, #condition);          \
                exit(__LINE__);                                              \
            }

#define TESTSET_MAGIC(testset)                                               \
            if (testset->magic != TEST_SET_MAGIC)                            \
            {                                                                \
                TestSet_output_magic(__FILE__, __LINE__,   __PRETTY_FUNC__,  \
               "TestSet", testset->magic, TEST_SET_MAGIC );                  \
                exit(__LINE__);                                              \
            }

#define TESTCASE_MAGIC(testcase)                                             \
            if (testcase->magic != TEST_CASE_MAGIC)                          \
            {                                                                \
                TestSet_output_magic(__FILE__, __LINE__,   __PRETTY_FUNC__,  \
                "TestSet", testcase->magic, TEST_CASE_MAGIC );               \
                exit(__LINE__);                                              \
            }


/*
 *  Module type definitions
 */
#ifndef __cplusplus
    typedef int bool;
#endif

/*
 *  Module global data
 */

/*
 *  Module local data
 */
static int passed = 0;
static int failed = 0;
static int total = 0;
static int nr_of_test_sets = 0;

static jmp_buf environment_buffer;

static TestSet default_test_set =
      { TEST_SET_MAGIC, "default", 0, 0, 0, 0, false, false, 0, 0, 0, 0, 0, 0, 0, 0 };
static TestSet* current_test_set = &default_test_set;
static TestSet destructed_test_set =
      { TEST_SET_DESTRUCTED, "", -1, -1, -1, -1, false, false, 0, 0, 0, 0, 0, 0, 0, 0 };

static int expected_signal = 0;
static int caught_signal = 0;


/*
 *  Module local functions
 */

/*#----------------------------------------------------------------------#*/
static void TestSet_execute_test(void)

/*
 *  Description : Execute the current test case
 *
 *  Input(s)    : -
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available variables:
 *                - test_set            to access the currect test case
 *                - expected signal     to ease building test cases which
 *                                      generate a signal
 *                - environment buffer  the signal jump environment buffer
 *
 *  Pre.Cond.   : signal handler should be installed (not checked!)
 *  Post.Cond.  : the current test cases has be executed and the statistics
 *                have beed updated according to the result
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    TESTSET_ENSURE(current_test_set != 0);
    TESTSET_MAGIC(current_test_set);
    TESTSET_ENSURE(current_test_set->current != 0 );
    TESTCASE_MAGIC(current_test_set->current);
    TESTSET_ENSURE(current_test_set->current->test != 0 );

    bool result = !OK;
    bool show_after = true;
    const char* test_case_name = current_test_set->current->name ?
                                 current_test_set->current->name : "<no name>";

    try {
        expected_signal = caught_signal = 0;
        TRACE("before setjump");
        if ( setjmp(environment_buffer) == 0 )
        {
            TRACE("after setjump=0");
            // call the setup_once function if defined and not yet called
            if ( current_test_set->setup_once_called ) {
                result = show_after = current_test_set->setup_once_result;
            }  else {
            	if ( current_test_set->setup_once ) {
                    result = current_test_set->setup_once_result
                           = current_test_set->setup_once();
                    if ( result != OK ) {
                    	TestSet_output_setup_once_failed( current_test_set->name );
                    	show_after = false;
                    }

            	} else {
            		result = current_test_set->setup_once_result = true;
            	}
            	current_test_set->setup_once_called = true;
        	}

            // call the setup function if defined
            if( result && current_test_set->setup ) {
            	 result = current_test_set->setup();
                    if ( result != OK ) {
                    	TestSet_output_setup_failed( current_test_set->current->name );
                    	show_after = false;
                    }            }

            if( result ) {
            	TestSet_output_test_case_before(test_case_name);
                result = current_test_set->current->test();
            }

            //  if expected_signal was set, it should have be caught
            if( expected_signal != 0 )
               result = !OK;

            // call the teardown function if defined
            if ( current_test_set->teardown )
            	 current_test_set->teardown();

            // call the teardown once function if defined and the last test
            if( ( current_test_set->current->next == 0 ) && current_test_set->teardown_once )
            	 current_test_set->teardown_once();

        }
        else
        {
            TRACE( "Result="            << result          );
            TRACE( "expected signal = " << expected_signal );
            TRACE( "caught signal = "   << caught_signal   );
            //  in case of no signals, they will be both zero
            if(  (expected_signal != caught_signal) )
                result = !OK;
            else
                result = OK;
        }
    }
    catch( std::exception except )
    {
        TestCase* test_case = current_test_set ? current_test_set->current
                                                 ? current_test_set->current : 0
                                               : 0;
        const char* test_case_name = test_case ? test_case->name : "<no name>";

        TestSet_output_unhandled_exception( test_case_name, except.what() );
        result = !OK;
    }
    catch( ... )
    {
        TestCase* test_case = current_test_set ? current_test_set->current
                                                 ? current_test_set->current : 0
                                               : 0;
        const char* test_case_name = test_case ? test_case->name : "<no name>";

        TestSet_output_unhandled_exception( test_case_name, 0 );
        result = !OK;
    }

    if( show_after )
    	TestSet_output_test_case_after(test_case_name, result);

    current_test_set->passed += (result == OK);
    current_test_set->failed += (result != OK);
    current_test_set->total++;
}

/*#----------------------------------------------------------------------#*/
static void TestSet_execute_test_set_tests(void)

/*
 *  Description : Executes the test cases
 *
 *  Input(s)    : -
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available test_set
 *  Pre.Cond.   : signal handler should be installed (not checked!)
 *  Post.Cond.  : all test cases should be executed.
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    TESTSET_ENSURE(current_test_set != 0);
    TESTSET_MAGIC(current_test_set);
    TESTSET_ENSURE(current_test_set->current != 0);
    TESTCASE_MAGIC(current_test_set->current);
    TESTSET_ENSURE(current_test_set->current->test != 0);

    while( current_test_set->current != NULL)
    {
        TESTCASE_MAGIC(current_test_set->current);
        TESTSET_ENSURE(current_test_set->current->test != 0);

        TestSet_execute_test();

        /*
         *  Make sure that after a test case the memory structure is in
         */
        TESTSET_ENSURE(current_test_set != 0);
        TESTSET_MAGIC(current_test_set);
        TESTSET_ENSURE(current_test_set->current != 0);
        TESTCASE_MAGIC(current_test_set->current);
        TESTSET_ENSURE(current_test_set->current->test != 0);

        current_test_set->current = current_test_set->current->next;
    }
}



/*#----------------------------------------------------------------------#*/
static void TestSet_handle_signal(int a_signal)

/*-
 *  Description : Catches and handles signals if necessary
 *
 *  Input(s)    : signal_nr: signal which causes this handler to be invoked
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : -
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 *
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    if( expected_signal != a_signal ) {
    	TestSet_output_handle_unexpected_signal( a_signal );
    }

    /* Checks to verify that the test set memory is not corrupted */
    TESTSET_ENSURE(current_test_set != 0);
    TESTSET_MAGIC(current_test_set);
    if(  current_test_set->current != NULL )
    {
        TESTSET_ENSURE(current_test_set->current != 0);
        TESTCASE_MAGIC(current_test_set->current);
        TESTSET_ENSURE(current_test_set->current->test != 0);

        (void) signal( a_signal, TestSet_handle_signal );
        caught_signal = a_signal;
    }

    TRACE( "before longjump" );
    longjmp( environment_buffer, 0 );
    TRACE( "after longjump" );

    exit(a_signal);
}


/*#----------------------------------------------------------------------#*/
static void TestSet_install_handlers(void)

/*-
 *  Description : Installs the signal handlers for the required signals
 *
 *  Input(s)    : -
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : SIGKILL can't be caught (and that is also not usefull
 *                because run away tests must remain killable)
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    int catched_signals[] = {
    	1,2,3,4,5,6,7,8,/*SIGKILL,*/10,
    	11,12,13,14,15,16,17,18,/*SIGSTOP,*/20,
    	21,22,23,24,25,26,27,28,29,30,31,
        SIGABRT,
        SIGFPE,
        SIGILL,
        SIGSEGV,
        SIGILL,   // SIGBUS,
        SIGTERM,
        22,       // SIGABRT in devcpp
        NSIG //SIGKILL  // Not catched, last resort when test fails recursively
    };
    int index = 0;

    /* now we can reset the signal handler, execute the next test case */
    while ( catched_signals[index] != NSIG ) //SIGKILL )
    {
        (void) signal( catched_signals[index], TestSet_handle_signal );
        index++;
    }
#ifdef GCC
    std::set_terminate (__gnu_cxx::__verbose_terminate_handler)
#endif
}


/*#----------------------------------------------------------------------#*/
static void TestSet_execute_test_set()

/*-
 *  Description : Execute all test cases belonging to the test set
 *
 *  Input(s)    : -
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Te test set itself contains the administration like
 *                the number of executed test cases, failed and passed etc.
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    TESTSET_ENSURE( current_test_set != 0 );
    TESTSET_MAGIC( current_test_set );

    if( current_test_set->nr_of_testcases < 1 ) {

        // the default test set can be ignored silently
        if( current_test_set != &default_test_set )
        {
            const char* test_set_name = current_test_set->name ?
                                        current_test_set->name : "<noname>";

            TestSet_output_test_set_before(test_set_name,
                                           current_test_set->nr_of_testcases);
        }
    }
    else
    {
        const char* test_set_name = current_test_set->name
        							?  current_test_set->name : "<noname>";
        TestSet_output_test_set_before(test_set_name,
                                       current_test_set->nr_of_testcases);
        current_test_set->current = current_test_set->head;

        // use setjmp to jump back here if anything fails
        TestSet_execute_test_set_tests();

        TestSet_output_test_set_after(test_set_name,
                                      current_test_set->passed,
                                      current_test_set->failed,
                                      current_test_set->total);
        nr_of_test_sets++;
    }

    passed += current_test_set->passed;
    failed += current_test_set->failed;
    total += current_test_set->total;
}


/*
 *  Module Exported functions
 */

/*#----------------------------------------------------------------------#*/
extern TestSet* TestSet_add_set( TestSet* a_test_set )

/*
 *  Description : Adds a new test set to the test sets
 *
 *  Input(s)    : a_test_set - the test set to be added
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   :
 *  Post.Cond.  :
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    /* Checks to verify that the test set memory is not corrupted */
    TESTSET_ENSURE(current_test_set != 0);
    TESTSET_ENSURE(a_test_set != 0);
    TESTSET_MAGIC(current_test_set);
    TESTSET_MAGIC(a_test_set);
    TESTSET_ENSURE(current_test_set != a_test_set);

    current_test_set->next = a_test_set;  // default is the start
    current_test_set = a_test_set;
    nr_of_test_sets++;

    return 0;
}


/*#----------------------------------------------------------------------#*/
extern TestCase* TestSet_add_test_case( TestCase* a_test_case )

/*
 *  Description : Adds a test case to the current test set
 *
 *  Input(s)    : a_test_case - the test case to be added
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    /* Checks to verify that the test set memory is not corrupted */
    TESTSET_ENSURE(current_test_set != 0);
    TESTSET_MAGIC(current_test_set);
    TESTSET_ENSURE(a_test_case != 0);
    TESTCASE_MAGIC(a_test_case);

    if( current_test_set->tail == 0)
    {
        current_test_set->head = current_test_set->tail = a_test_case;
    }
    else
    {
        current_test_set->tail->next = a_test_case;
        current_test_set->tail = a_test_case;
    }
    current_test_set->nr_of_testcases++;

    return NULL;
}

/*#----------------------------------------------------------------------#*/
extern TestSetupFunction TestSet_set_setup(TestSetupFunction setup_function)

/*
 *  Description : Adds a test case to the current test set
 *
 *  Input(s)    : setup_function - setup function for each test in this test_set
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    /* Checks to verify that the test set memory is not corrupted */
    TESTSET_ENSURE(current_test_set != 0);
    TESTSET_MAGIC(current_test_set);
    TESTSET_ENSURE(setup_function != 0);

    // TODO: make a sensible error message if setup is already set
    TESTSET_ENSURE(current_test_set->setup == 0);
    current_test_set->setup = setup_function;

    return NULL;

}


/*#----------------------------------------------------------------------#*/
extern TestTeardownFunction TestSet_set_teardown(TestTeardownFunction
		                                                     teardown_function )

/*
 *  Description : Adds a test case to the current test set
 *
 *  Input(s)    : teardown_function - teardown function for each test in this
 *                test_set
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    /* Checks to verify that the test set memory is not corrupted */
    TESTSET_ENSURE(current_test_set != 0);
    TESTSET_MAGIC(current_test_set);
    TESTSET_ENSURE(teardown_function != 0);

    // TODO: make a sensible error message if teardown is already set
    TESTSET_ENSURE(current_test_set->teardown == NULL);
    current_test_set->teardown = teardown_function;

    return NULL;
}

/*#----------------------------------------------------------------------#*/
extern TestSetupFunction TestSet_set_setup_once(TestSetupFunction
		                                            setup_once_function)

/*
 *  Description : Adds a test case to the current test set
 *
 *  Input(s)    : setup_once_function - setup function for each test_set
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    /* Checks to verify that the test set memory is not corrupted */
    TESTSET_ENSURE(current_test_set != 0);
    TESTSET_MAGIC(current_test_set);
    TESTSET_ENSURE(setup_once_function != 0);

    // TODO: make a sensible error message if setup is already set
    TESTSET_ENSURE(current_test_set->setup_once == 0);
    current_test_set->setup_once = setup_once_function;

    return NULL;

}


/*#----------------------------------------------------------------------#*/
extern TestTeardownFunction TestSet_set_teardown_once(TestTeardownFunction
		                                                teardown_once_function )

/*
 *  Description : Adds a test case to the current test set
 *
 *  Input(s)    : teardown_once_function - teardown function for each test_set
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : Uses the for this file globally available current_test_set
 *  Pre.Cond.   : -
 *  Post.Cond.  : -
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    /* Checks to verify that the test set memory is not corrupted */
    TESTSET_ENSURE(current_test_set != 0);
    TESTSET_MAGIC(current_test_set);
    TESTSET_ENSURE(teardown_once_function != 0);

    // TODO: make a sensible error message if teardown is already set
    TESTSET_ENSURE(current_test_set->teardown_once == NULL);
    current_test_set->teardown_once = teardown_once_function;

    return NULL;
}


/*#----------------------------------------------------------------------#*/
 void TestSet_expect_signal(int a_signal)

/*-
 *  Description : Catches and handles signals if necessary
 *
 *  Input(s)    : a_signal: signal number to be generated by the test case
 *  Output(s)   : -
 *  Returns     : -
 *  Notes       : -
 *  Pre.Cond.   : the signal may not already have been set
 *  Post.Cond.  : the signal has been set
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    // output is generated in the macros because abort/assert signal combination

    // Checks to verify correct behaviour
    TESTSET_ENSURE(a_signal > 0);
    TESTSET_ENSURE(a_signal <= NSIG);
    TESTSET_ENSURE(expected_signal == 0);

    expected_signal = a_signal;
}


/*#----------------------------------------------------------------------#*/
int TestSet_execute()

/*-
 *  Description : Execute all test cases belonging to the test set
 *
 *  Input(s)    : -
 *  Output(s)   : -
 *  Returns     : nr of failed test cases
 *  Notes       :
 *  Pre.Cond.   : test cases and test sets have to have been build
 *  Post.Cond.  : all test cases are run and test run statistics
 *                have been build
 */

/*#----------------------------------------------------------------------#*/
{
    TRACE_FUNC;

    TestSet* next_test_set;

    TestSet_install_handlers();

    current_test_set = &default_test_set;

    TestSet_output_all_test_sets_before(nr_of_test_sets);
    nr_of_test_sets = 0;

    while ( current_test_set != 0 )
    {
        /* use setjmp to jump back here if anything fails */
        if( setjmp(environment_buffer) == 0)
        {
            TestSet_execute_test_set();
        }
        next_test_set = current_test_set->next;
        *current_test_set = destructed_test_set;
        current_test_set = next_test_set;
    }

    TestSet_output_all_test_sets_after(passed, failed, total, nr_of_test_sets);

    return failed;
}

/* End of Document */
